// Handle Telnet Connections according to RFC 854

// Telnet commands
const SE = '\xf0'
const NOP = '\xf1';
const DATA_MARK = '\xf2';
const BREAK = '\xf3';
const INTERRUPT_PROCESS = '\xf4';
const ABORT_OUTPUT = '\xf5';
const ARE_YOU_THERE = '\xf6';
const ERASE_CHARACTER = '\xf7';
const ERASE_LINE = '\xf8';
const GO_AHEAD  = '\xf9';
const SB = '\xfa';

// Option commands
const WILL  = '\xfb';
const WONT  = '\xfc';
const DO = '\xfd';
const DONT = '\xfe';
const IAC = '\xff';

// Telnet options
const ECHO  = '\x01';
const SUPRESS_GO_AHEAD = '\x03';
const TERM_TYPE = '\x18';
const IS = '\x00';
const SEND = '\x01';
const NAWS = '\x1f';

// state
const STATE_DATA=0;
const STATE_IAC=1;
const STATE_WILL=2;
const STATE_WONT=3;
const STATE_DO=4;
const STATE_DONT=5;
const STATE_SB=6;

const SOCKET_APP_ID = "cnmgcaggjdeglolimdhmidbcnohjgeia";
const BBSFOX_EXT_ID = "odembcomamnalolekaeejamohkjfeemj";

function dump(str) {
    dumpLog(DUMP_TYPE_LOG, str);
}

function BFChecker() {
  this.InitTestTimer = null;
  this.InitCb = null;
}
BFChecker.prototype={
  init: function (initcb)
  {
    var cb = initcb;
    var _this = this;
    chrome.management.get(SOCKET_APP_ID, function(result){
    	if(result)
    	{
    		if(result.enabled)
    		{
    			if(result.version == "0.0.4") //only this version ok, need a white list
    			{
    			}
    			else
    			{
    				alert(msg("old_socket_version"));
    				return;
    			}
    		}
    		else
    		{
    			alert(msg("need_enable_socket"));
    			return;
    		}
    	}
    	else
    	{
    		alert(msg("need_install_socket"));
    		return;
    	}
    });
        
    chrome.runtime.sendMessage(
      SOCKET_APP_ID,
      {
        action: "status"
      },
      function(response)
      {
        if(!response)
        {
          chrome.management.launchApp(SOCKET_APP_ID);
          _this.InitCb = cb;
          _this.InitTestTimer = setTimer(true, function(){_this.onInitTest();}, 200);
        }
        else
        {
          cb();
        }
      }
    )
  },
  close: function(){
    if(_this.InitTestTimer)
      _this.InitTestTimer.cancel();
    _this.InitTestTimer = null;
  },
  onInitTest: function()
  {
    var _this = this;
    if(_this.InitTestTimer)
      _this.InitTestTimer.cancel();
    _this.InitTestTimer = null;

    chrome.runtime.sendMessage(
      SOCKET_APP_ID,
      {
        action: "status"
      },
      function(response)
      {
        if(!response)
        {
          _this.InitTestTimer = setTimer(true, function(){_this.onInitTest();}, 200);
        }
        else
        {
          _this.InitCb();
        }
      }
    )
  }
}

function TelnetCore(listener) {
    this.tran = null;
    this.ins = null;
    this.outs = null;
    this.host = null;
    this.port = 23;

    this.listener=listener;

    this.state=STATE_DATA;
    this.iac_sb='';
    //this.b52k3uao=window.uaotable;
    this.EscChar='\x15'; // Ctrl-U
    this.termType='VT100';
    this.lineWrap=0;
    this.initial=true;
    this.utf8Buffer=[];

    //AutoLogin - start
    this.autoLoginStage=0;
    this.loginPrompt=['','',''];
    this.loginStr=['','','',''];
    //AutoLogin - end

    this.socket = null;
    this.bfchecker = null;
    this.asciitable = window.asciiTable;
}

TelnetCore.prototype={

    soc_init: function ()
    {
    	if(this.bfchecker)
    	{
    		this.bfchecker.close();
    		this.bfchecker = null;
    	}
      var _this = bbsfox.telnetCore;
      //var tabid = document.getElementById('cmdHandler').getAttribute('mapcode');
      //_this.so.connect(_this.host, _this.port, tabid);
      _this.socket = chrome.runtime.connect(SOCKET_APP_ID);
      _this.socket.onMessage.addListener(function(msg) {
        switch(msg.action) {
            case "connected":
                _this.onStartRequest();
                break;
            case "onReceive":
                _this.ins.writeBuffer(msg.data);
                _this.onDataAvailable(null, null, _this.ins, 0, _this.ins.buffer.length);
                break;
            case "disconnected":
                _this.onStopRequest();
                break;
            default:
        }
      }.bind(_this));
      
      _this.socket.onDisconnect.addListener(function(msg) {
          _this.onStopRequest();
      }.bind(_this));
      
      var tabid = document.getElementById('cmdHandler').getAttribute('mapcode');
      _this.socket.postMessage({
            action: "connect",
            host: _this.host,
            port: _this.port,
            tabid: tabid
      });
    },

    oconv: {
      charset: '',
      ConvertFromUnicode: function(unicode_str) {
        if(this.charset !=  'big5')
          return ''; // Not implemented
        return u2b(unicode_str);
      }
    },

    connect: function(host, port) {
      //dumpLog(DUMP_TYPE_LOG, "telnet://" + host + ":" + port);
      if(host) {
        this.host = host;
        this.port = port;
      }

      var conn = this;
      this.ins = {
            buffer: '',
            writeBuffer: function(str) {
                this.buffer = str;//String.fromCharCode.apply(null, u8);
            },
            readBytes: function() {
                var buffer = this.buffer;
                this.buffer = '';
                return buffer;
            },
            close: function() {}
      };
      this.outs = {
            buffer: '',
            write: function(str, length) {
                this.buffer += str;
                //conn.so.send(str, length);
            },
            flush: function() {
                var output = this.buffer;
                conn.socket.postMessage({ action: "send", data: output });
                this.buffer = '';
            },
            close: function() {
                if(!conn.socket)
                    return;
                conn.socket.postMessage({ action: "disconnect" });
                conn.socket = null;
            }
      };
      this._ins = {};
      this.trans = {};

      // Check AutoLogin Stage
      //this.listener.loadLoginData(); //load login data
      if(this.loginStr[1])
        this.autoLoginStage = this.loginStr[0] ? 1 : 2;
      else if(this.loginStr[2])
        this.autoLoginStage = 3;
      else
        this.autoLoginStage = 0;

      this.closeConfirm();
      //this.initialAutoLogin();
      this.bfchecker = new BFChecker();
      this.bfchecker.init(this.soc_init);
    },

    close: function() {
    	if(this.bfchecker)
    	{
    		this.bfchecker.close();
    		this.bfchecker = null;
    	}    	
      this.so.close();
      this.so = null;
    },
    // data listener
    onStartRequest: function(){
      if(this.listener)
        this.listener.onConnect(this);
    },

    onStopRequest: function(){
      if(this.listener)
        this.listener.onClose(this);
    },

    onDataAvailable: function(req, ctx, ins, off, count) {
        var data='';
        // dump(count + 'bytes available\n');
        while(count > 0) {
            var s = this.ins.readBytes(count);
            count -= s.length;
            // dump(count + 'bytes remaining\n');
            var n=s.length;
            // this.oconv.charset='big5';
            // dump('data ('+n+'): >>>\n'+ this.oconv.ConvertToUnicode(s) + '\n<<<\n');
            for(var i = 0;i<n; ++i) {
                var ch=s[i];
                switch(this.state) {
                case STATE_DATA:
                    if( ch == IAC ) {
                        if(data) {
                            this.listener.onData(this, data);
                            data='';
                        }
                        this.state = STATE_IAC;
                    }
                    else
                        data += ch;
                    break;
                case STATE_IAC:
                    switch(ch) {
                    case WILL:
                        this.state=STATE_WILL;
                        break;
                    case WONT:
                        this.state=STATE_WONT;
                        break;
                    case DO:
                        this.state=STATE_DO;
                        break;
                    case DONT:
                        this.state=STATE_DONT;
                        break;
                    case SB:
                        this.state=STATE_SB;
                        break;
                    default:
                        this.state=STATE_DATA;
                    }
                    break;
                case STATE_WILL:
                    switch(ch) {
                    case ECHO:
                    case SUPRESS_GO_AHEAD:
                        this.send( IAC + DO + ch );
                        break;
                    default:
                        this.send( IAC + DONT + ch );
                    }
                    this.state = STATE_DATA;
                    break;
                case STATE_DO:
                    switch(ch) {
                    case TERM_TYPE:
                        this.send( IAC + WILL + ch );
                        break;
                    case NAWS:
                        this.send( IAC + WILL + ch );
                        this.sendNaws();
                        break;
                    default:
                        this.send( IAC + WONT + ch );
                    }
                    this.state = STATE_DATA;
                    break;
                case STATE_DONT:
                case STATE_WONT:
                    this.state = STATE_DATA;
                    break;
                case STATE_SB: // sub negotiation
                    this.iac_sb += ch;
                    if( this.iac_sb.slice(-2) == IAC + SE ) {
                        // end of sub negotiation
                        switch(this.iac_sb[0]) {
                        case TERM_TYPE: {
                            // FIXME: support other terminal types
                            //var termType = this.listener.prefs.TermType;
                            var rep = IAC + SB + TERM_TYPE + IS + this.termType + IAC + SE;
                            this.send( rep );
                            break;
                            }
                        }
                        this.state = STATE_DATA;
                        this.iac_sb = '';
                        break;
                    }
                }
            }
            if(data) {
                this.listener.onData(this, data);
                data='';
            }
        }
    },

    send: function(str) {
      if(str) {
        if(this.listener && this.outs)
        {
          this.listener.resetUnusedTime();
          this.outs.write(str, str.length);
          this.outs.flush();
        }
      }
    },

    convSend: function(unicode_str, charset, extbuf) {
        // supports UAO
        var s;
        // when converting unicode to big5, use UAO.
        {
            this.oconv.charset=charset;
            s = this.oconv.ConvertFromUnicode(unicode_str);
        }
        if(s)
            this.send(s);
    },

    lineWrapSend: function(s) {
      /*
        var counter = 0;
        var str = ''; // output texts
        var strbuf = ''; // buffer for storing alphanumerics
        for(var i = 0; i < s.length; ++i) {
            var ch = s.charAt(i);
            var len = 1;
            if(this.listener.buf.isFullWidth(ch)) len = 2; // DBCS
            else if(ch == '\t') len = 4;
            else if(ch.match(/[\v\r\n\f]/)) len = 0;
            if(ch.match(/\s/) || ch.match(/[^\x00-\x7f]/)) { // not alphanumerics
                if(ch.match(/[ \t]/)) { // should be at the end of lines
                    strbuf += ch;
                    counter += len;
                }
                if(counter > this.lineWrap) {
                    str += this.listener.view.EnterChar + strbuf;
                    counter = strbuf.length;
                    strbuf = '';
                    // strbuf is too large
                    if(counter > this.lineWrap) {
                        if(ch != '\r' && ch != '\n')
                            str += this.listener.view.EnterChar;
                        counter = 0;
                    }
                } else {
                    str += strbuf;
                    strbuf = '';
                }
                if(ch.match(/[ \t]/)) continue;
                counter += len;
                if(ch == '\r' || ch == '\n') counter = 0;
                if(counter > this.lineWrap) {
                    str += this.listener.view.EnterChar;
                    counter = len;
                }
                str += ch;
            } else { // alphanumerics
                strbuf += ch;
                ++counter;
            }
        }
        str += (counter > this.lineWrap ? this.listener.view.EnterChar : '') + strbuf;
        strbuf = '';
        this.convSend(str, this.listener.view.charset);
      */
    },

    sendNaws: function() {
        var cols = this.listener.buf ? this.listener.buf.cols : 80;
        var rows = this.listener.buf ? this.listener.buf.rows : 24;
        var nawsStr = String.fromCharCode(Math.floor(cols/256), cols%256, Math.floor(rows/256), rows%256).replace(/(\xff)/g,'\xff\xff');
        var rep = IAC + SB + NAWS + nawsStr + IAC + SE;
        this.send( rep );
    },

    closeConfirm: function() {
      /*
        if(this.listener.prefs.AskForClose && this.ins) {
      */
            //window.onbeforeunload = function() { return document.title; } // Warning in AMO
            var _this = this;
            this.beforeunload = function(e) {
                _this.socket.postMessage({ action: "beforeunload" });
                e.preventDefault();
            };
            window.addEventListener('beforeunload', this.beforeunload, false);
      /*
        } else {
            //window.onbeforeunload = null; // Warning in AMO
            if(this.beforeunload) {
                window.removeEventListener('beforeunload', this.beforeunload, false);
                delete this.beforeunload;
            }
        }
      */
    },

    utf8Data: function(str) {
      /*
        this.oconv.charset = 'UTF-8';
        var utf8Str = '';
        var output = new Array();
        for(var i=0; i<str.length; ++i)
            output.push(str.charCodeAt(i));
        try {
            utf8Str = this.oconv.convertFromByteArray(output, output.length);
        } catch(e) { // not UTF-8, return as ansi
            // dump('Cannot convert data('+output.join('+')+') to utf8\n');
            if(this.utf8Buffer.length < 3) {
                utf8Str = str; // hiding this may result in disorder of termbuf
            } else {
                for(var i=2; i>=0; --i) {
                    try {
                        output.unshift(this.utf8Buffer[i]);
                        utf8Str = this.oconv.convertFromByteArray(output, output.length);
                        break;
                    } catch(ex) {
                        if(i == 0) // Cannot recover this string
                            utf8Str = str;
                    }
                }
            }
        }
        this.utf8Buffer = this.utf8Buffer.concat(output);
        if(this.utf8Buffer.length > 3)
            this.utf8Buffer = this.utf8Buffer.slice(-3);
        return utf8Str;
      */
    },

    utf8Output: function(str) {
      /*
        this.oconv.charset = 'UTF-8';
        var result = this.oconv.convertToByteArray(str, {});
        var input = '';
        for(var i=0; i<result.length; ++i)
            input += String.fromCharCode(result[i]);
        return input;
      */
    },

    checkAutoLogin: function(row) {
        if(this.autoLoginStage > 3 || this.autoLoginStage < 1) {
          this.autoLoginStage = 0;
          return;
        }

        var line = this.listener.buf.getRowText(row, 0, this.listener.buf.cols);
        if(line.indexOf(this.loginPrompt[this.autoLoginStage - 1]) < 0)
          return;

        var Encoding = this.listener.view.charset;
        this.send(this.convSend(this.loginStr[this.autoLoginStage-1]+this.listener.view.EnterChar, Encoding, true));
        if(this.autoLoginStage == 3) {
          if(this.loginStr[3])
            this.send(this.convSend(this.loginStr[3], Encoding, true));
          this.autoLoginStage = 0;
          return;
        }
        ++this.autoLoginStage;
    }
}
